﻿using NuGet.Common;
using NuGet.Configuration;
using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;

namespace NuGetManager.CommunityToolkit.NuGet
{
    /// <summary>
    /// 
    /// </summary>
    public class NuGetPackageManager
    {
        private NuGetFramework? _nuGetFramework;

        // ReSharper disable once MemberCanBePrivate.Global
        /// <summary>
        /// 
        /// </summary>
        /// <param name="packagesInstallFolder"></param>
        /// <param name="source"></param>
        public NuGetPackageManager(string packagesInstallFolder, string source)
        {
            _packagesInstallFolder = packagesInstallFolder;
            _source = source;
        }

        /// <summary>
        /// 
        /// </summary>
        public NuGetFramework? NuGetFramework
        {
            get
            {
                if (_nuGetFramework == null) _nuGetFramework = NuGetFramework.ParseFolder("net6");
                return _nuGetFramework;
            }
        }

        private ISettings? _settings;

        private ISettings? Settings
        {
            get
            {
                if (_settings != null) return _settings;
                _settings = global::NuGet.Configuration.Settings.LoadDefaultSettings(root: null);
                return _settings;
            }
        }
        private string _packagesInstallFolder;

        private string PackagesInstallFolder
        {
            get
            {
                if (string.IsNullOrEmpty(_packagesInstallFolder)) _packagesInstallFolder = AppDomain.CurrentDomain.BaseDirectory;
                return _packagesInstallFolder;
            }
        }
        private string _source;

        private string Source
        {
            get
            {
                if (string.IsNullOrEmpty(_source)) _source = "https://api.nuget.org/v3/index.json";
                return _source;
            }
        }



        /// <summary>
        /// 
        /// </summary>
        public async Task ListPackageVersionsAsync()
        {
            // This code region is referenced by the NuGet docs. Please update the docs if you rename the region
            // or move it to a different file.
            #region ListPackageVersions
            ILogger logger = NullLogger.Instance;
            CancellationToken cancellationToken = CancellationToken.None;

            SourceCacheContext cache = new SourceCacheContext();
            SourceRepository repository = Repository.Factory.GetCoreV3(Source);
            FindPackageByIdResource resource = await repository.GetResourceAsync<FindPackageByIdResource>(cancellationToken);

            IEnumerable<NuGetVersion> versions = await resource.GetAllVersionsAsync(
                "Newtonsoft.Json",
                cache,
                logger,
                cancellationToken);

            foreach (NuGetVersion version in versions)
            {
                Console.WriteLine($"Found version {version}");
            }
            #endregion
        }
        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        public async Task DownloadPackageAsync(string packageId, string strPackageVersion)
        {
            // This code region is referenced by the NuGet docs. Please update the docs if you rename the region
            // or move it to a different file.
            #region DownloadPackage
            ILogger logger = NullLogger.Instance;
            CancellationToken cancellationToken = CancellationToken.None;

            SourceCacheContext cache = new SourceCacheContext();
            SourceRepository repository = Repository.Factory.GetCoreV3(Source);
            FindPackageByIdResource resource = await repository.GetResourceAsync<FindPackageByIdResource>(cancellationToken);

            //string packageId = "Licensing.Clients.POIClient";
            NuGetVersion packageVersion = new NuGetVersion(strPackageVersion);
            var packagePathResolver = new PackagePathResolver(PackagesInstallFolder);
            var clientPolicyContext = global::NuGet.Packaging.Signing.ClientPolicyContext.GetClientPolicy(Settings, NullLogger.Instance);
            var packageExtractionContext = new PackageExtractionContext(PackageSaveMode.Defaultv3, XmlDocFileSaveMode.None, clientPolicyContext, NullLogger.Instance);
            using MemoryStream packageStream = new MemoryStream();

            await resource.CopyNupkgToStreamAsync(
                packageId,
                packageVersion,
                packageStream,
                cache,
                logger,
                cancellationToken);

            var r = await SearchPackages(packageId);



            var downloadResource = await repository.GetResourceAsync<DownloadResource>(CancellationToken.None);
            var packageIdentities = r as PackageIdentity[] ?? r.ToArray();
            var downloadResult = await downloadResource.GetDownloadResourceResultAsync(
                                packageIdentities.FirstOrDefault(),
                                new PackageDownloadContext(cache),
                                SettingsUtility.GetGlobalPackagesFolder(Settings),
                                NullLogger.Instance, CancellationToken.None);

            await PackageExtractor.ExtractPackageAsync(PackagesInstallFolder,
                downloadResult.PackageStream,
                packagePathResolver,
                packageExtractionContext,
                CancellationToken.None);

            await InstallPackage(AppDomain.CurrentDomain.BaseDirectory, packageIdentities.FirstOrDefault() ?? throw new InvalidOperationException(), true);

            Console.WriteLine($"Downloaded package {packageId} {packageVersion}");

            using PackageArchiveReader packageReader = new PackageArchiveReader(packageStream);
            NuspecReader nuspecReader = await packageReader.GetNuspecReaderAsync(cancellationToken);
            Console.WriteLine($"Tags: {nuspecReader.GetTags()}");
            Console.WriteLine($"Description: {nuspecReader.GetDescription()}");
            #endregion
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="targetFolder"></param>
        /// <param name="installedPath"></param>
        /// <param name="f"></param>
        /// <param name="loadDlls"></param>
        /// <exception cref="InvalidOperationException"></exception>
        private void InstallFile(string targetFolder, string installedPath, string f, bool loadDlls)
        {
            string source;
            string f2;
            string filename;
            string dir;
            string target;
            try
            {
                source = Path.Combine(installedPath, f);
                var arr = f.Split('/');
                if (arr[0] == "lib")
                {
                    if (arr.Length == 2)
                    {
                        f2 = f.Substring(f.IndexOf("/", 3, StringComparison.Ordinal) + 1);
                    }
                    else
                    {
                        f2 = f.Substring(f.IndexOf("/", 4, StringComparison.Ordinal) + 1);
                    }
                }
                else
                {
                    f2 = f.Substring(f.IndexOf("/", 0, StringComparison.Ordinal) + 1);
                }

                filename = Path.GetFileName(f2);
                dir = Path.GetDirectoryName(f2) ?? throw new InvalidOperationException();
                target = Path.Combine(targetFolder, dir, filename);
                if (!Directory.Exists(Path.Combine(targetFolder, dir)))
                {
                    Directory.CreateDirectory(Path.Combine(targetFolder, dir));
                }
                CopyIfNewer(source, target);
                if (Path.GetExtension(target) == ".dll" && loadDlls)
                {
                    try
                    {
                        //Log.Debug("NuGetPackageManager.InstallFile::Loading " + target);
                        var an = System.Reflection.AssemblyName.GetAssemblyName(target);
                        System.Reflection.Assembly.Load(an);
                    }
                    catch (Exception)
                    {
                        //Log.Error(ex.ToString());
                    }
                }
            }
            catch (Exception)
            {
                //Log.Error(ex.ToString());
            }
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="targetFolder"></param>
        /// <param name="identity"></param>
        /// <param name="loadDlls"></param>
        /// <returns></returns>
        public async Task<bool> InstallPackage(string targetFolder, PackageIdentity identity, bool loadDlls)
        {
            bool ret = true;

            var packagePathResolver = new PackagePathResolver(PackagesInstallFolder);
            var installedPath = packagePathResolver.GetInstalledPath(identity);

            PackageReaderBase packageReader = new PackageFolderReader(installedPath);
            var libItems = packageReader.GetLibItems();
            var frameworkReducer = new FrameworkReducer();
            var frameworkSpecificGroups = libItems as FrameworkSpecificGroup[] ?? libItems.ToArray();
            var files = frameworkSpecificGroups
                //.Where(x => x.TargetFramework.Equals(nearest))
                .SelectMany(x => x.Items).ToList();
            foreach (var f in files)
            {
                InstallFile(targetFolder, installedPath, f, loadDlls);
            }

            var cont = packageReader.GetContentItems();
            NuGetFramework nearest;
            files = cont
                //.Where(x => x.TargetFramework.Equals(nearest))
                .SelectMany(x => x.Items).ToList();
            foreach (var f in files)
            {
                InstallFile(targetFolder, installedPath, f, loadDlls);
            }

            try
            {
                var dependencies = packageReader.GetPackageDependencies();
                var packageDependencyGroups = dependencies as PackageDependencyGroup[] ?? dependencies.ToArray();
                nearest = frameworkReducer.GetNearest(NuGetFramework, packageDependencyGroups.Select(x => x.TargetFramework));
                foreach (var dep in packageDependencyGroups.Where(x => x.TargetFramework.Equals(nearest)))
                {
                    foreach (var p in dep.Packages)
                    {
                        var local = await GetLocal(p);
                        await InstallPackage(targetFolder, local, loadDlls);
                    }
                }
            }
            catch (Exception)
            {
                ret = false;
                //Log.Error(ex.ToString());
            }

            if (Directory.Exists(installedPath + @"\build"))
            {
                if (Directory.Exists(installedPath + @"\build\x64"))
                {
                    foreach (var f in Directory.GetFiles(installedPath + @"\build\x64"))
                    {
                        var filename = Path.GetFileName(f);
                        var target = Path.Combine(targetFolder, filename);
                        CopyIfNewer(f, target);
                    }
                }
            }

            return ret;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="dep"></param>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        public async Task<PackageIdentity> GetLocal(PackageDependency dep)
        {
            var searchResult = (await SearchPackages(dep.Id))
            .Where(r =>
            (!dep.VersionRange.IsMaxInclusive || r.Version.Version <= dep.VersionRange.MaxVersion.Version) &&
            (!dep.VersionRange.IsMinInclusive || r.Version.Version >= dep.VersionRange.MinVersion.Version)).ToList();
            SourceRepository repository = Repository.Factory.GetCoreV3(Source);
            SourceCacheContext cache = new SourceCacheContext();
            var packagePathResolver = new PackagePathResolver(PackagesInstallFolder);
            var clientPolicyContext = global::NuGet.Packaging.Signing.ClientPolicyContext.GetClientPolicy(Settings, NullLogger.Instance);
            var packageExtractionContext = new PackageExtractionContext(PackageSaveMode.Defaultv3, XmlDocFileSaveMode.None, clientPolicyContext, NullLogger.Instance);
            var downloadResource = await repository.GetResourceAsync<DownloadResource>(CancellationToken.None);

            var downloadResult = await downloadResource.GetDownloadResourceResultAsync(
                searchResult.FirstOrDefault(),
                new PackageDownloadContext(cache),
                SettingsUtility.GetGlobalPackagesFolder(Settings),
                NullLogger.Instance, CancellationToken.None);

            await PackageExtractor.ExtractPackageAsync(PackagesInstallFolder,
                downloadResult.PackageStream,
                packagePathResolver,
                packageExtractionContext,
                CancellationToken.None);
            return searchResult.FirstOrDefault() ?? throw new InvalidOperationException();

        }
        private void CopyIfNewer(string source, string target)
        {
            var infoOld = new FileInfo(source);
            var infoNew = new FileInfo(target);
            if (infoNew.LastWriteTime != infoOld.LastWriteTime)
            {
                try
                {
                    File.Copy(source, target, true);
                }
                catch (Exception)
                {
                    // ignored
                }
            }
        }


        /// <summary>
        /// 
        /// </summary>
        public async Task GetPackageMetadataAsync()
        {
            // This code region is referenced by the NuGet docs. Please update the docs if you rename the region
            // or move it to a different file.
            #region GetPackageMetadata
            ILogger logger = NullLogger.Instance;
            CancellationToken cancellationToken = CancellationToken.None;

            SourceCacheContext cache = new SourceCacheContext();
            SourceRepository repository = Repository.Factory.GetCoreV3(Source);
            PackageMetadataResource resource = await repository.GetResourceAsync<PackageMetadataResource>();

            IEnumerable<IPackageSearchMetadata> packages = await resource.GetMetadataAsync(
                "Newtonsoft.Json",
                includePrerelease: true,
                includeUnlisted: false,
                cache,
                logger,
                cancellationToken);

            foreach (IPackageSearchMetadata package in packages)
            {
                Console.WriteLine($"Version: {package.Identity.Version}");
                Console.WriteLine($"Listed: {package.IsListed}");
                Console.WriteLine($"Tags: {package.Tags}");
                Console.WriteLine($"Description: {package.Description}");
            }
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        public async Task SearchPackages()
        {
            // This code region is referenced by the NuGet docs. Please update the docs if you rename the region
            // or move it to a different file.
            #region SearchPackages
            ILogger logger = NullLogger.Instance;
            CancellationToken cancellationToken = CancellationToken.None;

            SourceRepository repository = Repository.Factory.GetCoreV3(Source);
            PackageSearchResource resource = await repository.GetResourceAsync<PackageSearchResource>();
            SearchFilter searchFilter = new SearchFilter(includePrerelease: true);

            IEnumerable<IPackageSearchMetadata> results = await resource.SearchAsync(
                "json",
                searchFilter,
                skip: 0,
                take: 20,
                logger,
                cancellationToken);

            foreach (IPackageSearchMetadata result in results)
            {
                Console.WriteLine($"Found package {result.Identity.Id} {result.Identity.Version}");
            }
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="filter"></param>
        /// <returns></returns>
        public async Task<IEnumerable<PackageIdentity>> SearchPackages(string filter)
        {
            var list = new List<PackageIdentity>();
            // This code region is referenced by the NuGet docs. Please update the docs if you rename the region
            // or move it to a different file.
            #region SearchPackages
            ILogger logger = NullLogger.Instance;
            CancellationToken cancellationToken = CancellationToken.None;

            SourceRepository repository = Repository.Factory.GetCoreV3(Source);
            PackageSearchResource resource = await repository.GetResourceAsync<PackageSearchResource>();
            SearchFilter searchFilter = new SearchFilter(includePrerelease: true);

            IEnumerable<IPackageSearchMetadata> results = await resource.SearchAsync(
                filter,
                searchFilter,
                skip: 0,
                take: 20,
                logger,
                cancellationToken);

            foreach (IPackageSearchMetadata result in results)
            {
                list.Add(result.Identity);
                Console.WriteLine($"Found package {result.Identity.Id} {result.Identity.Version}");
            }
            return list;
            #endregion
        }

       
        /// <summary>
        /// 
        /// </summary>
        public void ReadPackage()
        {
            // This code region is referenced by the NuGet docs. Please update the docs if you rename the region
            // or move it to a different file.
            #region ReadPackage
            using FileStream inputStream = new FileStream("MyPackage.nupkg", FileMode.Open);
            using PackageArchiveReader reader = new PackageArchiveReader(inputStream);
            NuspecReader nuspec = reader.NuspecReader;
            Console.WriteLine($"ID: {nuspec.GetId()}");
            Console.WriteLine($"Version: {nuspec.GetVersion()}");
            Console.WriteLine($"Description: {nuspec.GetDescription()}");
            Console.WriteLine($"Authors: {nuspec.GetAuthors()}");

            Console.WriteLine("Dependencies:");
            foreach (var dependencyGroup in nuspec.GetDependencyGroups())
            {
                Console.WriteLine($" - {dependencyGroup.TargetFramework.GetShortFolderName()}");
                foreach (var dependency in dependencyGroup.Packages)
                {
                    Console.WriteLine($"   > {dependency.Id} {dependency.VersionRange}");
                }
            }

            Console.WriteLine("Files:");
            foreach (var file in reader.GetFiles())
            {
                Console.WriteLine($" - {file}");
            }
            #endregion
        }

        
       
    }
}
